項目50 ジェネリックを型に対する関数と考える
値空間では、関数はコードの重複を避ける1つの方法
型空間では、ジェネリック型が関数に相当し、型の重複を避ける方法になる
ジェネリック型は1つ以上の型パラメータを受け取り、具体的な非ジェネリック型を生成する
Partialジェネリック型は、ある型のすべてのプロパティをオプションにする
自分で定義するとしたら下記になる
code:ts
type MyPartial<T> = {K in keyof T?: TK}
複数の型パラメータを受け取る、ジェネリック型も書ける
型レベルプログラミングでも、TypeScriptは型チェッカーが働きエラーを検知する
code:ts
type MyPick<T, K> = {
P in K: TP
// ~ Type 'K' is not assignable to type 'string | number | symbol'.
// ~~~~ Type 'P' cannot be used to index type 'T'.
};
型レベルのエラーに対応する方法も存在する
一番単純な方法はエラーを無視すること
code:ts
// @ts-expect-error(これをやらないこと!)
type MyPick<T, K> = { P in K: TP };
type AgeOnly = MyPick<Person, 'age'>;
// ^? type AgeOnly = { age: number; }
TypeScriptが期待する型とのインターセクションを使う
この場合はインターセクションとしてPropertyKeyを使っている
code:ts
type MyPick<T, K> = { P in K & PropertyKey: TP & keyof T };
type AgeOnly = MyPick<Person, 'age'>;
// ^? type AgeOnly = { age: number; }
type FirstNameOnly = MyPick<Person, 'firstName'>;
// ^? type FirstNameOnly = { firstName: never; }
extendsキーワードを使って、型パラメータに制約を加える
Tがオブジェクトであることと、KがTのキーのサブタイプである制約を設けている
code:ts
type MyPick<T extends object, K extends keyof T> = {P in K: TP};
type AgeOnly = MyPick<Person, 'age'>;
// ^? type AgeOnly = { age: number; }
type FirstNameOnly = MyPick<Person, 'firstName'>;
// ~~~~~~~~~~~
// Type '"firstName"' does not satisfy the constraint 'keyof Person'.
type Flip = MyPick<'age', Person>;
// ~~~~~ Type 'string' does not satisfy the constraint 'object'.
ジェネリック型を定義する際は、何らかの制約を加えて安全性を向上できないかまず検討する
また、ジェネリック型にはTSDocを書くようにし、補完が効くようにすると良い
https://scrapbox.io/files/681b0da719a536e26f2fd6a2.png
型パラメータは関数やクラスのような、値レベルの構文でも定義できる
関数の場合
ジェネリック型のPickに相当するpick関数を作る
code:ts
function pick<T extends object, K extends keyof T>(
obj: T, ...keys: K[]
): Pick<T, K> {
const picked: Partial<Pick<T, K>> = {};
for (const k of keys) {
pickedk = objk;
}
return picked as Pick<T, K>;
}
const p: Person = { name: 'Matilda', age: 5.5 };
const age = pick(p, 'age');
// ^? const age: Pick<Person, "age">
console.log(age); // { age: 5.5 }がログに出力される
関数に型パラメータを導入することのメリットは、ジェネリック型や型レベル演算をしていることを利用者に隠蔽できる点
クラスの場合
ジェネリッククラスでは、型パラメータ(T)とそのクラスのプロパティとメソッドに関連付けるジェネリック型が導入できる
code:ts
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const dateBox = new Box(new Date());
// ^? const dateBox: Box<Date>
値空間では、他の関数をパラメータとして受け取る高階関数があるが、型レベルでこれを表現する方法は現段階ではない
#TypeScript